﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Windows.Input;
using ArchiSimple.Windows.Message;
using ArchiSimple.Windows.Model;
using ArchiSimple.Windows.ViewModel.PlanObjects;
using GalaSoft.MvvmLight.Command;
#if NETFX_CORE
using Windows.UI.Xaml.Controls;
using WindowsPoint = global::Windows.Foundation.Point;
using Windows.UI.Xaml.Media;
using Windows.UI;
#else
using System.Windows.Media;
using WindowsPoint = global::System.Windows.Point;
#endif

namespace ArchiSimple.Windows.ViewModel
{
    public class PlanViewModel : ViewModelBase
    {
        public enum PlanMode
        {
            SelectPlanObject,
            MovePlanObject,
            AddWall,
        }

        private const double SnaPointSize = 10;

        private const double SnapAlignSize = 10;

        private const double SnapGridSize = 100;

        private const double SnapWallSize = 5;

        public bool SnapToGrid
        {
            get { return this.Get<bool>("SnapToGrid"); }
            set { this.Set<bool>("SnapToGrid", value); }
        }

        public bool IsSnap
        {
            get { return this.Get<bool>("IsSnap"); }
            set { this.Set<bool>("IsSnap", value); }
        }

        public double MouseX
        {
            get { return this.Get<double>("MouseX"); }
            set { this.Set<double>("MouseX", value); }
        }

        public double MouseY
        {
            get { return this.Get<double>("MouseY"); }
            set { this.Set<double>("MouseY", value); }
        }

        public WindowsPoint MousePoint
        {
            get { return new WindowsPoint(this.MouseX, this.MouseY); }
            set
            {
                double xOfset = 0;
                double yOfset = 0;

                if (this.Mode == PlanMode.MovePlanObject)
                {
                    if (this.movePoints != null && this.movePoints.Length == 1)
                    {
                        var ofset = this.movePoints[0].Current.ToWindowsPoint().Substract(this.srcPoint);
                        xOfset = ofset.X;
                        yOfset = ofset.Y;
                    }
                }

                double x = value.X + xOfset;
                double y = value.Y + yOfset;

                SnapValue(ref x, ref y);

                this.MouseX = x - xOfset;
                this.MouseY = y - yOfset;
            }
        }

        private WindowsPoint srcPoint;

        private PointViewModel snapPoint = null;

        private WallViewModel snapWall = null;

        /// <summary>
        /// TODO : Make private this method.
        /// </summary>
        /// <param name="x"></param>
        /// <param name="y"></param>
        public void SnapValue(ref double x, ref double y)
        {
            this.IsSnap = false;
            this.snapPoint = null;
            this.snapWall = null;

            // if around points.
            // TODO : return the min snap for points.
            if (this.PlanObjects != null)
            {

                foreach (var item in this.PlanObjects.OfType<PointViewModel>())
                {
                    if (this.movePoints != null && this.movePoints.Contains(item))
                    {
                        continue;
                    }

                    if (this.addWall != null)
                    {
                        if (item == this.addWall.Begin || item == this.addWall.End)
                        {
                            continue;
                        }
                    }

                    var distX = item.X - x;
                    var distY = item.Y - y;

                    if (distX > -SnaPointSize && distX < SnaPointSize && distY > -SnaPointSize && distY < SnaPointSize)
                    {
                        x = item.X;
                        y = item.Y;
                        this.snapPoint = item;
                        this.IsSnap = true;
                        return;
                    }
                }
            }

            // Test alignement with walls of the current point.
            IEnumerable<PointViewModel> snapPoints = null;
            if (this.addWall != null && this.addWall.Begin != null)
            {
                // If create a new wall. Test alignement with the begin point.
                snapPoints = new[] { this.addWall.Begin };
            }
            else if (this.movePoints != null && this.movePoints.Length == 1)
            {
                // If move a point. Test alignement with this wall.
                snapPoints = this.movePoints[0].NextPoints;
            }

            if (snapPoints != null)
            {
                foreach (var item in snapPoints)
                {
                    var distX = item.X - x;
                    var distY = item.Y - y;

                    if (distX > -SnapAlignSize && distX < SnapAlignSize)
                    {
                        x = item.X;
                        this.IsSnap = true;
                    }
                    else if (distY > -SnapAlignSize && distY < SnapAlignSize)
                    {
                        y = item.Y;
                        this.IsSnap = true;
                    }
                }
            }

            if (this.PlanObjects != null)
            {
                foreach (var wall in this.PlanObjects.OfType<WallViewModel>())
                {
                    if (wall.Begin == null || wall.End == null)
                    {
                        continue;
                    }

                    if (wall.Begin.X == wall.End.X)
                    {
                        if (y > Math.Min(wall.Begin.Y, wall.End.Y)
                            && y < Math.Max(wall.Begin.Y, wall.End.Y))
                        {
                            var distX = wall.Begin.X - x;

                            if (distX > -SnapAlignSize && distX < SnapAlignSize)
                            {
                                x = wall.Begin.X;
                                this.snapWall = wall;
                                this.IsSnap = true;
                                return;
                            }
                        }
                    }

                    if (wall.Begin.Y == wall.End.Y)
                    {
                        if (x > Math.Min(wall.Begin.X, wall.End.X)
                            && x < Math.Max(wall.Begin.X, wall.End.X))
                        {
                            var distY = wall.Begin.Y - y;

                            if (distY > -SnapAlignSize && distY < SnapAlignSize)
                            {
                                y = wall.Begin.Y;
                                this.snapWall = wall;
                                this.IsSnap = true;
                                return;
                            }
                        }
                    }

                    if (x > Math.Min(wall.Begin.X, wall.End.X)
                        && x < Math.Max(wall.Begin.X, wall.End.X)
                        && y > Math.Min(wall.Begin.Y, wall.End.Y)
                        && y < Math.Max(wall.Begin.Y, wall.End.Y))
                    {
                        var a1 = 1.0 * (wall.Begin.Y - wall.End.Y) / (wall.Begin.X - wall.End.X);

                        var b1 = wall.Begin.Y - a1 * wall.Begin.X;

                        var xSnap = (y + x / a1 - b1) / (a1 + 1 / a1);
                        var distX = xSnap - x;

                        if (distX >= -SnapWallSize && distX <= SnapWallSize)
                        {
                            var ySnap = a1 * xSnap + b1;
                            var distY = ySnap - y;

                            if (distY >= -SnapWallSize && distY <= SnapWallSize)
                            {
                                y = ySnap;
                                x = xSnap;
                                this.snapWall = wall;
                                this.IsSnap = true;
                                return;
                            }
                        }
                    }
                }
            }

            if (this.IsSnap)
            {
                return;
            }

            // TODO : if snap grid is actif.

            // At the end return the value without snap.
        }

        public ObservableCollection<PlanObjectViewModel> PlanObjects
        {
            get { return this.Get<ObservableCollection<PlanObjectViewModel>>("PlanObjects"); }
            set { this.Set<ObservableCollection<PlanObjectViewModel>>("PlanObjects", value); }
        }

        public virtual ICommand AddWallCommand { get; set; }

        public virtual ICommand UpPlanCommand { get; set; }

        public virtual ICommand DownPlanCommand { get; set; }

        public virtual ICommand PreviewWallCommand { get; set; }

        public PlanMode Mode
        {
            get { return this.Get<PlanMode>("Mode"); }
            set { this.Set<PlanMode>("Mode", value); }
        }

        private Plan plan;

        public PlanObjectViewModel CurrentPlanObject
        {
            get { return this.Get<PlanObjectViewModel>("CurrentPlanObject"); }
            set
            {
                var point = value as PointViewModel;
                var wall = value as WallViewModel;
                var room = value as RoomViewModel;
                if (point != null)
                {
                    this.movePoints = new[] { point };
                }
                else if (wall != null)
                {
                    this.movePoints = new[] { wall.Begin, wall.End };
                }
                else if (room != null)
                {
                    this.movePoints = room.Points;
                }
                else
                {
                    this.movePoints = null;
                }

                this.Send<SelectPlanObjectMessage>(new SelectPlanObjectMessage { ViewModel = value });

                this.Set<PlanObjectViewModel>("CurrentPlanObject", value);
            }
        }

        private WallViewModel addWall = null;

        private PointViewModel[] movePoints;

        public PathGeometry WallGeometry
        {
            get
            {
                if (this.PlanObjects != null)
                {
                    var walls = from w in this.PlanObjects.OfType<WallViewModel>()
                                where w.WallGeometry != null
                                select w.WallGeometry;

                    var path = new PathGeometry { Figures = new PathFigureCollection() };

                    foreach (var w in walls)
                    {
                        path.Figures.Add(w);
                    }

                    return path;
                }

                return null;
            }
        }

        private Dictionary<PlanObject, PlanObjectViewModel> dicoPlanObject = new Dictionary<PlanObject, PlanObjectViewModel>();

        public PlanObjectViewModel CreateOrGet(PlanObject model)
        {
            if (model == null)
            {
                return null;
            }

            if (dicoPlanObject.ContainsKey(model))
            {
                return dicoPlanObject[model];
            }
            else
            {
                var vm = PlanObjectViewModel.Create(this, model);

                dicoPlanObject.Add(model, vm);

                return vm;
            }
        }

        /// <summary>
        /// TODO : Make this private
        /// </summary>
        /// <param name="p"></param>
        public void LoadPlanMessage(LoadPlanMessage p)
        {
            this.plan = p.Plan;
            if (p.Plan.PlanObjects == null)
            {
                p.Plan.PlanObjects = new List<PlanObject>();
            }

            this.PlanObjects = (from o in p.Plan.PlanObjects
                                select this.CreateOrGet(o))
                                .ToObservable();

            this.RaisePropertyChanged(() => PlanObjects);
            this.RaisePropertyChanged(() => WallGeometry);
        }

        public override void Init()
        {
            base.Init();

            this.Register<LoadPlanMessage>(LoadPlanMessage);

            this.AddWallCommand = new RelayCommand(() =>
            {
                this.Mode = PlanMode.AddWall;
                var model = new Wall();
                var viewModel = this.CreateOrGet(model);

                AddToPlan(viewModel);
                this.addWall = viewModel as WallViewModel;
                this.CurrentPlanObject = null;
            });

            this.DownPlanCommand = new RelayCommand(() =>
            {
                if (this.CurrentPlanObject != null)
                {
                    this.srcPoint = this.MousePoint;
                    this.Mode = PlanMode.MovePlanObject;
                }
            });

            this.UpPlanCommand = new RelayCommand(() =>
            {
                if (this.Mode == PlanMode.MovePlanObject)
                {
                    this.Mode = PlanMode.SelectPlanObject;
                }

                if (this.Mode == PlanMode.AddWall)
                {
                    if (this.addWall.Begin != null)
                    {
                        this.Mode = PlanMode.SelectPlanObject;

                        // The end point is set on PreviewWall.
                        // If the end point equal to Zero
                        if (this.addWall.End == null)
                        {
                            RemovePointFromPlan(this.addWall.Begin, 0);
                            RemoveFromPlan(this.addWall);
                            this.addWall = null;
                            return;
                        }

                        if (this.addWall.Begin.X == this.addWall.End.X && this.addWall.Begin.Y == this.addWall.End.Y)
                        {
                            RemovePointFromPlan(this.addWall.Begin);
                            RemovePointFromPlan(this.addWall.End);
                            RemoveFromPlan(this.addWall);
                            this.addWall = null;
                            return;
                        }

                        // Verify End point instance.

                        if (this.snapPoint != null)
                        {
                            RemovePointFromPlan(this.addWall.End);
                            this.addWall.End = this.snapPoint;
                        }
                        else if (this.snapWall != null)
                        {
                            AddPointToRoom(this.snapWall, this.addWall.End);

                            var newWall = new Wall();
                            var vmNewWall = this.CreateOrGet(newWall) as WallViewModel;
                            vmNewWall.Begin = this.addWall.End;
                            vmNewWall.End = this.snapWall.End;
                            AddToPlan(vmNewWall);
                            this.snapWall.End = this.addWall.End;

                            RaisePropertyChanged(() => WallGeometry);
                        }

                        this.UpdateRooms(this.addWall);

                        this.CurrentPlanObject = null;
                        this.addWall = null;
                        return;
                    }

                    if (this.snapPoint != null)
                    {
                        // Point exists use the same.
                        this.addWall.Begin = snapPoint;
                        return;
                    }

                    // Create point.
                    var point = new Point { X = this.MouseX, Y = this.MouseY };
                    var vmPoint = this.CreateOrGet(point) as PointViewModel;
                    AddToPlan(vmPoint);

                    // Add begin point.
                    this.addWall.Begin = vmPoint;

                    if (this.snapWall != null)
                    {
                        AddPointToRoom(this.snapWall, this.addWall.Begin);

                        var newWall = new Wall();
                        var vmNewWall = this.CreateOrGet(newWall) as WallViewModel;
                        vmNewWall.Begin = this.addWall.Begin;
                        vmNewWall.End = this.snapWall.End;
                        AddToPlan(vmNewWall);
                        this.snapWall.End = this.addWall.Begin;

                        RaisePropertyChanged(() => WallGeometry);
                        return;
                    }
                }
            });

            this.PreviewWallCommand = new RelayCommand(() =>
            {
                if (this.Mode == PlanMode.MovePlanObject)
                {
                    var move = this.MousePoint.Substract(this.srcPoint);

                    foreach (var point in this.movePoints)
                    {
                        point.X += move.X;
                        point.Y += move.Y;
                    }

                    this.srcPoint = this.MousePoint;
                }

                if (this.Mode == PlanMode.AddWall)
                {
                    if (this.addWall != null && this.addWall.Begin != null)
                    {
                        if (this.addWall.End == null)
                        {
                            // Create point.
                            var point = new Point { X = this.MouseX, Y = this.MouseY };
                            var vmPoint = this.CreateOrGet(point) as PointViewModel;
                            AddToPlan(vmPoint);

                            this.addWall.End = vmPoint;

                            RaisePropertyChanged(() => WallGeometry);
                        }
                        else
                        {
                            this.addWall.End.X = this.MouseX;
                            this.addWall.End.Y = this.MouseY;
                        }
                    }
                }
            });
        }

        private void AddPointToRoom(WallViewModel wall, PointViewModel cutPoint)
        {
            foreach (var item in this.PlanObjects.OfType<RoomViewModel>())
            {
                int beginIndex = item.Points.IndexOf(p => p.X == wall.Begin.X
                    && p.Y == wall.Begin.Y);

                if (beginIndex >= 0)
                {
                    int endIndex = item.Points.IndexOf(p => p.X == wall.End.X
                        && p.Y == wall.End.Y);

                    if (endIndex >= 0)
                    {
                        var room = item;

                        if (beginIndex + 1 == endIndex || beginIndex == endIndex + 1)
                        {
                            room.InsertPointAfter(Math.Min(beginIndex, endIndex) + 1, cutPoint);
                        }
                        else
                        {
                            room.InsertPointAfter(Math.Max(beginIndex, endIndex) + 1, cutPoint);
                        }
                    }
                }
            }
        }

        private void RemovePointFromPlan(PointViewModel viewmodel, int minNextPoints = 1)
        {
            // Don't delete the point from the plan, if there is more one wall used.
            if (viewmodel.NextPoints.Count <= minNextPoints)
            {
                this.plan.PlanObjects.Remove(viewmodel.PlanObject);
                this.PlanObjects.Remove(viewmodel);
            }
        }

        public void RemoveFromPlan(PlanObjectViewModel viewmodel)
        {
            this.plan.PlanObjects.Remove(viewmodel.PlanObject);
            this.PlanObjects.Remove(viewmodel);
        }

        /// <summary>
        /// TODO : Make private this method.
        /// </summary>
        /// <param name="viewmodel"></param>
        public void AddToPlan(PlanObjectViewModel viewmodel)
        {
            this.plan.PlanObjects.Add(viewmodel.PlanObject);
            this.PlanObjects.Add(viewmodel);
        }

        private class PointGraph
        {
            public PointViewModel EndPoint { get; set; }

            public PointViewModel BeginPoint { get; set; }

            public List<PointGraph> PreviousPoint { get; set; }

            public PointViewModel PointViewModel { get; set; }

            public List<PointGraph> NextPoints
            {
                get
                {
                    var q = from p in this.PointViewModel.NextPoints
                            where this.PreviousPoint.All(p2 => p2.PointViewModel != p)
                            && (!p.NextPoints.Where(p2 => p2 != this.PointViewModel && (p2 != this.BeginPoint || p != this.EndPoint)).Any(p2 => this.PreviousPoint.Any(p3 => p3.PointViewModel == p2))
                            )
                            select new PointGraph
                            {
                                EndPoint = this.EndPoint,
                                BeginPoint = this.BeginPoint,
                                PreviousPoint = this.PreviousPoint.Union(new[] { this }).ToList(),
                                PointViewModel = p,
                            };

                    return q.ToList();
                }
            }
        }

        private void UpdateRooms(WallViewModel newWall)
        {
            if (newWall.Begin.NextPoints.Count > 1 && newWall.End.NextPoints.Count > 1)
            {
                Room baseRoom = null;

                var walls = this.PlanObjects.OfType<WallViewModel>().ToArray();

                var beginPoint = newWall.Begin;
                var endPoint = newWall.End;
                Queue<PointGraph> pointQueue = new Queue<PointGraph>();
                var nextPoints = (from p in beginPoint.NextPoints
                                  where p != endPoint
                                  select new PointGraph
                                  {
                                      EndPoint = endPoint,
                                      BeginPoint = beginPoint,
                                      PreviousPoint = new[] 
                                      { 
                                          new PointGraph
                                          {  
                                              EndPoint= endPoint,
                                              BeginPoint = beginPoint,
                                              PreviousPoint = new List<PointGraph>(),
                                              PointViewModel= beginPoint,
                                          }
                                      }.ToList(),
                                      PointViewModel = p,
                                  });

                // Detect if new wall cut a room.
                var roomsCut = from r in this.PlanObjects.OfType<RoomViewModel>()
                               where r.Current.Points.Contains(newWall.Begin.Current)
                               && r.Current.Points.Contains(newWall.End.Current)
                               select r;

                foreach (var room in roomsCut.ToList())
                {
                    baseRoom = room.Current;
                    RemoveFromPlan(room);
                }

                // Get next points. (Used wall to have the next points)
                foreach (var child in nextPoints)
                {
                    pointQueue.Enqueue(child);
                }

                while (pointQueue.Count != 0)
                {
                    var point = pointQueue.Dequeue();

                    if (point.PointViewModel == endPoint)
                    {
                        // Add room.
                        Room room;
                        if (baseRoom == null)
                        {
                            room = new Room() { Color = Colors.White };
                        }
                        else
                        {
                            room = new Room { Title = baseRoom.Title, Color = baseRoom.Color };
                        }

                        var points = (from p in point.PreviousPoint
                                      select p.PointViewModel.Current).ToList();
                        points.Add(point.PointViewModel.Current);

                        // Add points to the room.
                        room.Points = points;
                        var vmRoom = this.CreateOrGet(room);

                        this.AddToPlan(vmRoom);
                    }
                    else
                    {
                        nextPoints = point.NextPoints;

                        // Get next points. (Used wall to have the next points)
                        foreach (var child in nextPoints)
                        {
                            pointQueue.Enqueue(child);
                        }
                    }
                }
            }
        }
    }
}
